Skip to content

feat(onboarding): a fresh install always reaches a workspace; no provider CLI is required (#995) - #1047

Merged
Juliusolsson05 merged 8 commits into
mainfrom
fix/onboarding-first-run
Sep 20, 2026
Merged

Juliusolsson05 merged 8 commits into
mainfrom
fix/onboarding-first-run

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Closes #995. Built to docs/decomposition/onboarding-first-run.md; its Status section maps each stage to its artifact and states the deviations.

The wall, recorded

Stage 1 runs the real checkPrerequisites() on a simulated clean Mac. Only the machine's edges are simulated:

On main (82babd21) both clean machines returned ready:false, blocking:[claude,codex]. That includes the packaged app, where OpenCode is bundled and usable, and a machine where Grok was found. Recordings: testing/fixtures/first-run/, baseline baseline-main-82babd21.json.

What changes

  • No tool blocks launch. Provider required is gone. src/shared/setup/readiness.ts is the one policy: usableProviders, plus firstSessionKind, which is the default provider if usable, else the first usable one, else a terminal. checkPrerequisites stamps both onto the result, and the gate, bootstrap and pickers read those fields.
  • Setup panel:
    • It opens by itself only when no provider is usable, or when a Homebrew-installable helper is missing.
    • Each missing provider shows a copyable install command (its native installer; every URL was checked) and a docs link. The manual path override stays.
    • "Continue with a terminal" is the explicit acknowledgment. Escape does not decide it.
  • Reopenable: the palette command Open Setup and File › Setup…. The spawn error now names that place; before, it said "open Setup", which did not exist after launch.
  • Bootstrap:
    • A fresh install waits for the first-run decision instead of spawning Claude underneath the gate. That spawn used to fail and leave no tabs and autosave off.
    • It opens firstSessionKind and falls back to a terminal if that spawn fails.
    • If the user presses Retry after installing a CLI, the first project is that agent.
  • cwd: the first project is home when launchd started the app in /.
  • Pickers: missing providers read "Not installed · File › Setup…" but stay selectable, since a probe can be wrong (Portability: 'any Mac, any user' hardening (works-on-my-machine audit — Tier A) #495 A1). The ⌘T path picker preselects the provider the machine actually has.

Tests

  • testing/system/first-run/prerequisites.firstRun.test.ts re-runs both clean simulations live on every push. It fails if a provider row drifts from its recording, and it asserts the policy on the live result. On the macOS CI runner, which has no provider CLI, that is the genuine zero-provider Mac.

  • readiness.test.ts: the policy over the recorded machines. The packaged clean Mac gets opencode, Grok-only gets grok, zero providers gets a terminal, the developer machine gets claude, and every machine the old gate locked out had something to run.

  • firstRun.renderer.test.tsx: the real useWorkspace and bootstrap plus the real SetupGate, fed the recorded checks. It covers:

    • no provider: panel, install commands, no spawn until the user answers;
    • Continue gives a terminal and restoreStatus: fresh;
    • Retry after installing gives an agent;
    • the packaged app opens the bundled OpenCode;
    • a failed spawn falls back to a terminal;
    • Open Setup reopens and re-probes.

    Fail-first: 5 of the 6 fail against main's useBootstrap.ts.

  • Path picker preselect and hint on the packaged recording, defaultWorkspaceCwd, and catalog governance: 124 commands and File-menu plus Setup.

  • 1,330 tests pass across the touched areas, and npx tsc -b is clean.

Not in this PR

🤖 Generated with Claude Code

Juliusolsson05 and others added 4 commits September 19, 2026 02:13
…ean Mac (#995 stage 1)

The recorder runs the REAL checkPrerequisites with only the machine's
edges simulated:
- a fresh HOME, where every provider CLI installs on macOS;
- launchd's PATH;
- /bin/sh;
- a temp Electron app path, which for the packaged case holds the
  OpenCode runtime tree #994 ships.

The recordings were taken on main 82babd2, before any policy change,
and show the wall. Both clean machines report ready:false with
blocking [claude, codex], even the packaged one where OpenCode is
bundled and usable, and even though Grok was found.

Without RECORD_FIRST_RUN=1 the test re-runs both clean simulations live
and fails if a provider row no longer matches its recording, so the
fixtures cannot drift from what the probes report. Rows found at a
machine-wide path (this Mac's npm-global Grok under /opt/homebrew) are
recording-machine facts and are not compared. The README explains this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…first project (#995 stage 2)

Claude Code and Codex were `required`, so a Mac without both met a
SetupGate with no Continue button. The stage-1 recordings show the wall:
- the packaged app on a clean Mac, with OpenCode bundled and usable,
  was ready:false and blocked on [claude, codex];
- so was a Grok-only machine.

Changes:
- `required` is removed from provider descriptors. SetupToolStatus.provider
  marks what the gate must treat as an agent CLI, without the lockout.
- src/shared/setup/readiness.ts is the one policy:
  - usableProviders: found, bundled included;
  - firstSessionKind: the default provider if usable, else the first
    usable one in registry order, else a terminal.
  checkPrerequisites stamps both onto SetupCheckResult, so no consumer
  re-derives them. ready/blocking are gone; nothing blocks.
- Each provider carries a copyable install command and docs URL. These are
  the providers' native installers, since a fresh Mac has no npm or brew.
  Each URL was checked on 2026-09-19. Grok's only published distribution
  is npm.

Tests:
- The recorder now saves the whole result as `check`. The probe rows are
  identical to the baseline recording, and the pre-#995 verdicts are kept
  in baseline-main-82babd21.json.
- readiness.test.ts runs the policy over the recorded machines:
  - packaged clean Mac → opencode;
  - Grok-only → grok;
  - zero providers → terminal, via the one documented edit that unsets
    machine-wide installs;
  - developer machine → claude.
- The live system check asserts the policy on the real probe result. On
  the macOS CI runner that is the genuine zero-provider Mac.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…up can be reopened (#995)

Stages 3–6 of docs/decomposition/onboarding-first-run.md.

- Setup store (features/setup/store.ts): one shared check for the gate,
  the bootstrap and the pickers, with one in-flight probe.
- SetupGate:
  - It opens by itself only when no provider is usable, or when a
    Homebrew-installable helper is missing.
  - "Continue with a terminal" is the explicit acknowledgment. Escape
    does not decide it.
  - Missing providers show a copyable install command and a docs link.
    The manual path override now applies to any missing provider.
- Reopenable: the `open-setup` command, and File › Setup… (added to
  NATIVE_MENU_COMMAND_IDS). The spawn error names that place instead of
  "open Setup", which did not exist after launch.
- Bootstrap:
  - A fresh install waits for the first-run decision instead of spawning
    Claude under the gate.
  - It opens firstSessionKind, and falls back to a terminal if that spawn
    fails, so the run always has a project and autosave.
  - The persisted-fallback recovery shell uses the same verdict without
    waiting.
- The first project's cwd is home when launchd started the app in /.
- Pickers (path picker, new agent, new agent in, provider switch):
  - Missing providers get a "Not installed · File › Setup…" hint but stay
    selectable, since a probe can be wrong.
  - The path picker preselects the machine's first usable provider.

Tests: firstRun.renderer.test.tsx runs the real useWorkspace and bootstrap
and the real SetupGate against the RECORDED checks. The cases:
- no provider: panel, install commands, no spawn until answered;
- a terminal on Continue, and restoreStatus fresh;
- Retry after installing gives an agent;
- the packaged app opens the bundled OpenCode;
- a failed spawn falls back to a terminal;
- Open Setup reopens and re-probes.
Fail-first: 5 of 6 fail against main's bootstrap. Also added:
- PathPickerModal preselect and hint tests on the packaged recording;
- defaultWorkspaceCwd tests;
- catalog governance: 124 commands, 43 approved additions, and the File
  menu plus Setup.

Closes #995

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 19, 2026
…1047

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Juliusolsson05 and others added 4 commits September 19, 2026 18:28
…utton does

Review of #1047 (CHANGES REQUESTED):

1. The panel stamped the interaction-owner marker and said aria-modal
   but never moved focus. The keyboard router's ownership branch does
   not stop propagation and a terminal pane forwards keys straight to
   its PTY, so with focus left in an agent pane a user could type — and
   press Enter — into the live shell underneath a panel that looked
   modal, and Tab walked out into the background UI. It now composes the
   shared Dialog primitive, which owns focus containment, the inert
   background, the marker and Escape (components/ui/README.md makes that
   the primitive's job), and the hand-rolled window listener is gone.
2. "Continue with a terminal" recorded the skipped optional helpers
   BEFORE closing, inside the same try. A setup.json write failure (a
   full disk, a read-only state dir) therefore left the panel up with no
   Escape and the fresh-install bootstrap waiting on an answer that could
   never arrive — the lockout class #995 removes. The close moved to
   finally: recording a skip is best effort, the acknowledgment is not.
3. The button promised a terminal project in cases where none would be
   opened (a returning user with a restored workspace; a panel the user
   opened whose re-probe found no provider). The store now tracks whether
   a fresh-install bootstrap is parked on the answer, and only then does
   the button read "Continue with a terminal"; otherwise it is Continue
   or Close. Escape and click-outside are refused only in that same
   state, instead of for every automatic panel.
4. A failed install's output is capped and scrollable: unbounded, it
   pushed the footer and its only button past the viewport.

Tests: focus containment and the inert background, the skip failure
still answering the panel, and the Close wording when nothing waits. All
three fail against the previous gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mber the answer

Codex review of #1047 (policy/bootstrap lens), CHANGES REQUESTED:

1. BLOCKING. The panel tells users to run OpenCode's installer, which
   writes ~/.opencode/bin and appends its PATH export to ~/.zshrc. The
   login-shell probe runs `$SHELL -lc`, and `zsh -lc` never sources
   .zshrc, so the directory was invisible to both resolution layers: the
   gate said "Not installed", the user ran the command it gave them,
   pressed Retry, and it still said "Not installed" — the one loop this
   feature exists to close. WELL_KNOWN_BIN_DIRS now includes it, and
   ~/.grok/bin for the same reason. A system test installs a stub there
   and fails without the change.
2. The provider-less answer is persisted in setup.json beside the
   skipped helpers. In memory only, it reopened the modal on every
   launch AND in every new window, each being its own renderer process.
3. Only the panel that ASKED records an answer. Closing a panel opened
   from the menu used to durably skip mitmproxy, while Escape in the
   same panel recorded nothing.
4. open-setup no longer closes a panel a parked bootstrap is waiting on:
   a menu item called Setup… must not silently decide "continue with a
   terminal" when Escape and click-outside are refused on purpose.
5. resetSetupStoreForTests settles a parked waiter, so one test's
   bootstrap can no longer resolve into the next one.
6. The live policy assertion is concrete per environment instead of
   restating firstSessionKindFor, which could only fail for a stamping
   mismatch.
7. The fixtures README says that recording rewrites the developer's real
   setup.json, and the plan records the accepted probe-deadline residual.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…indow's bootstrap

Codex review of #1047, P1. awaitFirstRunDecision treated only a live
provider or this run's dismissal as an answer. A machine that had
already chosen "continue with a terminal" therefore parked forever when
a SECOND window opened: its workspace slice is absent, so bootstrap takes
the fresh path, while the panel correctly refused to show itself because
the answer was persisted — leaving nothing to release the wait.

One `decided()` predicate now covers all three ways the question is
already answered, and it is used both before parking and inside the
subscription. The new test opens such a window and expects a terminal
project with no dialog.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The catalog governance counts collide: main gained `enable-goal-loop-mcp`
(#1006) while this branch adds `open-setup` (#995). Both exist now, so
every pinned number moves by one — 125 commands, 119 literal, 44
approved additions — and the three arithmetic comments name both.

Also: `git add -A` re-staged this worktree's stale submodule pointers
over the ones the merge brought in, which `tsc -b` caught as a missing
jumpToLatest on OpencodeTerminalHeadless (#1043's package bump). Every
gitlink is back to main's commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit 653e106 into main Sep 20, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/onboarding-first-run branch September 20, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(onboarding): make a fresh install usable without pre-installed CLIs

1 participant